上一篇源码|HDFS之DataNode:写数据块(1)分析了无管道无异常情况下,datanode上的写数据块过程。本文分析管道写无异常的情况,假设副本系数3(即写数据块涉及1个客户端+3个datanode),未发生任何异常。
源码版本:Apache Hadoop 2.6.0
本文内容虽短,却是建立在前文的基础之上。对于前文已经说明的内容,本文不再赘述,建议读者按顺序阅读。
开始之前
总览
根据源码|HDFS之DataNode:写数据块(1),对于多副本的管道写流程,主要影响DataXceiver#writeBlock()、BlockReceiver#receivePacket()、PacketResponder线程三部分。本文按照这三个分支展开。
文章的组织结构
- 如果只涉及单个分支的分析,则放在同一节。
- 如果涉及多个分支的分析,则在下一级分多个节,每节讨论一个分支。
- 多线程的分析同多分支。
- 每一个分支和线程的组织结构遵循规则1-3。
建立管道:DataXceiver#writeBlock()
准备接收数据块:BlockReceiver.<init>()
|
|
与副本系数1的情况下相比,仅仅增加了“下游节点的处理”的部分:以当前节点为“客户端”,继续触发下游管道的建立;对于下游节点,仍然要走一遍当前节点的流程。当客户端收到第一个datanode管道建立成功的ack时,下游所有的节点的管道一定已经建立成功,加上客户端,组成了完整的管道。
另外,根据前文的分析,直到执行BlockReceiver.receiveBlock()才开始管道写数据块内容,结合管道的关闭过程,可知管道的生命周期分为三个阶段:
- 管道建立:以管道的方式向下游发送管道建立的请求,从下游接收管道建立的响应。
- 管道写:当客户端收到管道建立成功的ack时,才利用刚刚建立的管道开始管道写数据块的内容。
- 管道关闭:以管道的方式向下游发送管道关闭的请求,从下游接收管道关闭的响应。
如图说明几个参数:
- in:上游节点到当前节点的输入流,当前节点通过in接收上游节点的packet。
- replyOut::当前节点到上游节点的输出流,当前节点通过replyOut向上游节点发送ack。
- mirrorOut:当前节点到下游节点的输出流,当前节点通过mirrorOut向下游节点镜像发送packet。
- mirrorIn:下游节点到当前节点的输入流,当前节点通过mirrorIn接收下游节点的镜像ack。
请求建立管道:Sender#writeBlock()
Sender#writeBlock():
|
|
逻辑非常简单。为什么要去掉targets中的第一个节点?假设客户端发送的targets中顺序存储d1、d2、d3,当前节点为d1,那么d1的下游只剩下d2、d3,继续向下游发送管道建立请求时,自然要去掉当前targets中的第一个节点d1;d2、d3同理。
依靠这种targets逐渐减少的逻辑,DataXceiver#writeBlock()才能用targets.length > 0
判断是否还有下游节点需要建立管道。
客户端也使用Sender#writeBlock()建立管道。但发送过程略有不同:客户端通过自定义的字节流写入数据,需要将字节流中的数据整合成packet,再写入管道。
向下游管道发送packet:BlockReceiver#receivePacket()
同步接收packet:BlockReceiver#receivePacket()
先看BlockReceiver#receivePacket()。
严格来说,BlockReceiver#receivePacket()负责接收上游的packet,并继续向下游节点管道写:
|
|
由于已经在中建立了管道,接下来,管道写的工作非常简单,只涉及“管道写相关”部分:
每收到一个packet,就将in中收到的packet镜像写入mirrorOut;对于下游节点,仍然要走一遍当前节点的流程。
另外,BlockReceiver#shouldVerifyChecksum()也发挥了作用:管道的中间节点在落盘前不需要校验。
向上游管道响应ack:PacketResponder线程
异步发送ack:PacketResponder线程
与BlockReceiver#receivePacket()相对,PacketResponder线程负责接收下游节点的ack,并继续向上游管道响应。
PacketResponder#run():
|
|
前文一不小心分析了PacketResponder线程如何处理以管道的方式响应ack,此处简单复习,关注ack与pkt的关系。
总结起来,PacketResponder线程的核心工作如下:
- 接收下游节点的ack
- 比较ack.seqno与当前队头的pkt.seqno
- 如果相等,则向上游发送pkt
- 如果是最后一个packet,将block的状态转换为FINALIZED
一道面试题
早上碰巧看到一道面试题:
1个节点发送100G的数据到99个节点,硬盘、内存、网卡速度都是1G/s,如何时间最短?
猴子有篇笔记里分析了“管道写”技术的优势。如果熟悉HDFS中的“管道写”,就很容易解决该题:
单网卡1G/s,那么同时读写的速度最大500M/s。假设硬盘大于100G,内存大于1G,忽略零碎的建立管道、响应ack的成本,管道写一个100G大小的数据块,至少需要100G / (500M/s) = 200s
。
能不能继续优化呢?其实很容易估计,看集群中闲置资源还有多少。在管道写的方案中,两个节点间的带宽上始终占着500M数据,因此,只有管道中的头节点与尾节点剩余500M/s的带宽,其他节点的带宽都已经打满。因此,已经无法继续优化。
如果题目的资源并没有这么理想,比如硬盘读800M/s,写200M/s,那么明显管道写的速度最高也只能到200M/s,其他资源和假设不变,则至少需要100G / (200M/s) = 500s
。当然,实际情况比这里的假设要复杂的多,管道写的最大好处在于性能平衡,让每个节点的资源占用相当,不出现短板才可能发挥最大的优势。
- 忘记题目描述网卡1G/s,还是带宽1G/s。如果是后者,那么速度快一倍,至少需要100s。
- 题目还要求写出伪码。如果不考虑容错性,完全可以按照这两篇文章的分析,剥离出主干代码完成题目,猴子就不啰嗦了。
总结
引用一张图做总结:
了解了管道写的正常流程,下文将分析管道写中的部分错误处理策略。